package com.reuters.rfa.example.omm.domainServer.marketbyprice;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import com.reuters.rfa.common.Token;
import com.reuters.rfa.example.framework.prov.ProvDomainMgr;
import com.reuters.rfa.example.omm.domainServer.DataGenerator;
import com.reuters.rfa.example.omm.domainServer.DataStreamItem;
import com.reuters.rfa.example.utility.CommandLine;
import com.reuters.rfa.omm.OMMMapEntry;
import com.reuters.rfa.omm.OMMMsg;

/**
 * MarketByPriceGenerator provides a simple algorithm to generate data for
 * Market By Price domain. It can be used to create MarketByPriceStreamItems.
 * 
 * <p>
 * <b>How to generate response data.</b>
 * </p>
 * 
 * Unique prices(key) are generated during each iteration by decrementing and
 * incrementing the base price that is randomly generated at in the first round
 * of iteration. The key is generated by appending a 'B' to the price for BID
 * prices, and by appending an 'S' to the price for ASK prices The unique price,
 * The side (BID or ASK), aggregated size of orders, total number of orders, and
 * the action (update, add, or delete) are chosen randomly during message
 * updates to generate more realistic data. The time of the order is the current
 * time in which response data be created.
 * 
 * @see MarketByPriceStreamItem
 */
public class MarketByPriceGenerator implements DataGenerator
{

    private List<OrderEntry> _orderEntries;
    private List<OrderEntry> _modifiedEntries;
    private int _basePrice;
    private int _priceRange;

    // the maximum number of caches
    private int _maxEntries = 50;
    private Random _random;

    private int _deleteFactor = 30; // 30% chance of deleting a current entry
    // private int _addFactor = 30; // 30% chance of adding a current entry
    private int _updateFactor = 40; // 40% chance of updating a current entry

    private int _maxRetryRandom = 1000;
    private int _initOrderEntrySize = 25;
    private Set<String> _allOrderPrice;

    /**
     * Represents a single order entry
     */
    public static class OrderEntry implements Cloneable
    {

        public byte action; // UPDATE, ADD, DELETE
        public String orderId; // = orderPrice and orderSide (B for buy or S for
                               // Sell)
        public int orderPrice;
        public short orderSide; // BID = 1, ASK = 2
        public long orderSize;
        public long noOrder;
        public long quotim;

        public Object clone()
        {
            OrderEntry newEntry = new OrderEntry();
            newEntry.action = action;
            newEntry.orderId = orderId;
            newEntry.orderPrice = orderPrice;
            newEntry.orderSide = orderSide;
            newEntry.orderSize = orderSize;
            newEntry.noOrder = noOrder;
            newEntry.quotim = quotim;
            return newEntry;
        }
    }

    public MarketByPriceGenerator()
    {

        _orderEntries = new ArrayList<OrderEntry>(50);
        _modifiedEntries = new ArrayList<OrderEntry>(50);
        _random = new Random();
        _basePrice = (_random.nextInt(1000) + 1) * 100;
        _priceRange = (_basePrice * 30) / 100;
        _allOrderPrice = new HashSet<String>();

        int currentPrice = _basePrice - (_initOrderEntrySize / 2);
        for (int i = 0; i < _initOrderEntrySize; i++)
        {
            OrderEntry orderEntry = generateNewOrderEntry(currentPrice);
            _orderEntries.add(orderEntry);
            _allOrderPrice.add(orderEntry.orderPrice + "");
            currentPrice++;
        }
    }

    public DataStreamItem createStreamItem(ProvDomainMgr mgr, Token token, OMMMsg msg)
    {
        boolean encodeDataDef = CommandLine.booleanVariable("MARKET_BY_PRICE_encodeDataDef");

        MarketByPriceStreamItem streamItem = new MarketByPriceStreamItem(this, mgr, token, msg);
        streamItem.setEncodeDataDef(encodeDataDef);
        return streamItem;
    }

    public Object[] getInitialEntries()
    {
        return _orderEntries.toArray();
    }

    public Object[] getNextEntries()
    {
        return _modifiedEntries.toArray();
    }

    public void generateUpdatedEntries()
    {

        _modifiedEntries.clear();
        HashSet<String> newEntries = new HashSet<String>();

        for (int i = 0; i < 10; i++)
        {

            OrderEntry orderEntry = null;
            int action = _random.nextInt(100);

            if (_orderEntries.size() == 0 || action >= _deleteFactor + _updateFactor)
            {
                // Action Add
                if (_orderEntries.size() >= _maxEntries)
                {
                    continue;
                }
                int orderPrice = findAvailablePrice();
                if (orderPrice == 0)
                {
                    continue;
                }

                orderEntry = generateNewOrderEntry(orderPrice);
                _orderEntries.add(orderEntry);
                _allOrderPrice.add(orderEntry.orderPrice + "");
            }
            else if (action < _deleteFactor)
            {
                // Action Delete
                int index = _random.nextInt(_orderEntries.size());
                orderEntry = (OrderEntry)_orderEntries.get(index);

                if (newEntries.contains(orderEntry.orderId))
                {
                    continue;
                }

                orderEntry.action = OMMMapEntry.Action.DELETE;
                _orderEntries.remove(orderEntry);
                _allOrderPrice.remove(orderEntry.orderPrice + "");

            }
            else
            {
                // Action Update
                int index = _random.nextInt(_orderEntries.size());
                OrderEntry updateEntry = (OrderEntry)_orderEntries.get(index);

                if (newEntries.contains(updateEntry.orderId))
                {
                    continue;
                }

                updateOrder(updateEntry);
                orderEntry = (OrderEntry)updateEntry.clone();
                orderEntry.action = OMMMapEntry.Action.UPDATE;
            }

            newEntries.add(orderEntry.orderId);
            _modifiedEntries.add(orderEntry);
        }
    }

    /*
     * Finds the price that is not used before.
     */
    private int findAvailablePrice()
    {

        int randomPrice = 0;
        int retry = 0;
        while (true)
        {
            int price = _random.nextInt(_priceRange);
            retry++;
            if (_random.nextBoolean())
                randomPrice = _basePrice + price;
            else
                randomPrice = _basePrice - price;

            if (!_allOrderPrice.contains(randomPrice + "") || retry > _maxRetryRandom)
                break;
        }
        return randomPrice;
    }

    private OrderEntry generateNewOrderEntry(int orderPrice)
    {

        OrderEntry orderEntry = new OrderEntry();
        orderEntry.orderPrice = orderPrice;
        orderEntry.action = OMMMapEntry.Action.ADD;
        orderEntry.orderSize = _random.nextInt(1000000);
        orderEntry.noOrder = _random.nextInt(1000) + 1;
        long currentTime = System.currentTimeMillis();
        orderEntry.quotim = currentTime % (60 * 60 * 24 * 1000); // need midnight

        if (orderPrice < _basePrice)
        {
            orderEntry.orderSide = 1;
            orderEntry.orderId = orderEntry.orderPrice + "B";
        }
        else
        {
            orderEntry.orderSide = 2;
            orderEntry.orderId = orderEntry.orderPrice + "S";
        }
        return orderEntry;
    }

    private void updateOrder(OrderEntry orderEntry)
    {

        orderEntry.noOrder = _random.nextInt(1000) + 1;
        orderEntry.orderSize = _random.nextInt(1000000);
        long currentTime = System.currentTimeMillis();
        orderEntry.quotim = currentTime % (60 * 60 * 24 * 1000);
    }

}
